dash_charts.utils_static⚓︎
Utilities for generating static HTML reports.
View Source
"""Utilities for generating static HTML reports."""
import io
from base64 import b64encode
import dash_bootstrap_components as dbc
import dominate
import markdown
import pandas as pd
import plotly.io
from bs4 import BeautifulSoup
from dominate import tags, util
def write_div(figure, path_or_file_object, is_div=True, **html_kwargs):
"""Write Plotly figure as HTML to specified file.
Args:
figure: Plotly figure (can be from `create_figure` for custom charts)
path_or_file_object: *string* path or file object
is_div: if True (default) will override html_kwargs to only write the minimum HTML needed
html_kwargs: additional keyword arguments passed to `plotly.io.write_html()`
"""
for key in ['include_plotlyjs', 'full_html']:
if key not in html_kwargs and is_div:
html_kwargs[key] = False
plotly.io.write_html(fig=figure, file=path_or_file_object, **html_kwargs)
def make_div(figure, **html_kwargs):
"""Write Plotly figure as HTML to specified file.
Args:
figure: Plotly figure (can be from `create_figure` for custom charts)
html_kwargs: additional keyword arguments passed to `plotly.io.write_html()`
Returns:
str: HTML div
"""
with io.StringIO() as output:
write_div(figure, output, is_div=True, **html_kwargs)
return output.getvalue()
def add_image(image_path, alt_text=None):
"""Write base64 image to HTML.
Args:
image_path: Path to image file and format will be read from file suffix.
alt_text: alternate text. If None, will show the image filename
Returns:
str: HTML
"""
with open(image_path, 'rb') as image_file:
encoded_image = b64encode(image_file.read()).decode()
image_uri = f'data:image/{image_path.suffix[1:]};base64,{encoded_image}'
return f'<img src="{image_uri}" alt="{alt_text or image_path.name}"/>'
def add_video(video_path, alt_text=None):
"""Write base64 video to HTML.
Video formats can easily be converted with ffmpeg: `ffmpeg -i video_filename.mov video_filename.webm`
Args:
video_path: Path to video file and format will be read from file suffix. Video should be in webm format
alt_text: alternate text. If None, will show the video filename
Returns:
str: HTML video tag
"""
with open(video_path, 'rb') as video_file:
encoded_video = b64encode(video_file.read()).decode()
video_uri = f'data:video/{video_path.suffix[1:]};base64,{encoded_video}'
return f'<video src="{video_uri}" controls>{alt_text or video_path.name}</video>'
def write_image_file(figure, path_or_file_object, image_format, **img_kwargs):
"""Write Plotly figure as an image to specified file.
Args:
figure: Plotly figure (can be from `create_figure` for custom charts)
path_or_file_object: *string* path or file object
image_format: one of `(png, jpg, jpeg, webp, svg, pdf)`
img_kwargs: additional keyword arguments passed to `plotly.io.write_image()`
"""
plotly.io.write_image(fig=figure, file=str(path_or_file_object), format=image_format, **img_kwargs)
def capture_plotly_body():
"""Return HTML body that includes necessary scripts for Plotly and MathJax.
Returns:
tuple: of the top and the bottom HTML content
"""
# Capture necessary Plotly boilerplate HTML
with io.StringIO() as output:
write_div({}, output, is_div=False, include_mathjax='.js', validate=False)
blank_plotly = BeautifulSoup(output.getvalue(), features='lxml')
# Remove the empty figure div and corresponding script
plot_div = blank_plotly.find('div', attrs={'class': 'plotly-graph-div'})
for script in blank_plotly.find_all('script')[::-1]:
# Use the ID from the plot to identify which script needs to be removed
if plot_div.attrs['id'] in script.prettify():
script.decompose()
break
plot_div.decompose()
return blank_plotly.body.prettify()
def format_plotly_boilerplate(**doc_kwargs):
"""Initialize a boilerplate dominate document for creating static Plotly HTML files.
See dominate documentation: https://pypi.org/project/dominate/
Args:
doc_kwargs: keyword arguments for `dominate.document()`
Returns:
dict: dominate document instance
"""
doc = dominate.document(**doc_kwargs)
with doc:
util.raw(capture_plotly_body())
return doc
def create_dbc_doc(theme=dbc.themes.BOOTSTRAP, custom_styles='', **doc_kwargs):
"""Create boilerplate dominate document with Bootstrap and Plotly for static HTML.
Based on: https://github.com/facultyai/dash-bootstrap-components/tree/master/docs/templates/partials
See dominate documentation: https://pypi.org/project/dominate/
Args:
theme: string URL to CSS for theming Bootstrap. Default is `dbc.themes.BOOTSTRAP`
custom_styles: optional custom CSS to add to file. Default is blank (`''`)
doc_kwargs: keyword arguments for `dominate.document()`
Returns:
dict: dominate document instance
"""
stylesheets = [
{'href': 'https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/a11y-light.min.css'},
{'href': theme},
]
scripts = [
{'src': 'https://code.jquery.com/jquery-3.4.1.slim.min.js'},
{'src': 'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js'},
{'src': 'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js'},
{'src': 'https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js'},
]
doc = format_plotly_boilerplate(**doc_kwargs)
with doc.head:
tags.meta(charset='utf-8')
tags.meta(name='viewport', content='width=device-width, initial-scale=1')
for sheet_kwargs in stylesheets:
tags.link(rel='stylesheet', **sheet_kwargs)
util.raw(f'<style>{custom_styles}</style>')
for script_kwargs in scripts:
tags.script(**script_kwargs)
util.raw('<script>hljs.initHighlightingOnLoad();</script>')
return doc
def tag_code(text, language=''):
"""Format HTML for a `pre.code` block with specified `hljs` language.
Args:
text: string to show in code block
language: `hljs` language (ex: `'language-json'`). Default is `''`.
"""
tags.pre().add(tags.code(text, _class=f'{language} hljs'))
def tag_markdown(text, **markdown_kwargs):
"""Format markdown text as HTML.
Args:
text: markdown string
markdown_kwargs: additional keyword arguments for `markdown.markdown`, such as `extensions`
"""
util.raw(markdown.markdown(text, **markdown_kwargs))
def tag_table(df_table, table_class=None):
"""Format HTML for a responsive Bootstrap table.
See Bootstrap documentation at: https://getbootstrap.com/docs/4.4/content/tables/#tables
Args:
df_table: pandas dataframe to show in table
table_class: string classes to add to table. If None, will use default string
Raises:
RuntimeError: if `df_table` is not a DataFrame object
"""
if table_class is None:
table_class = 'table table-bordered table-striped table-hover'
if not isinstance(df_table, pd.core.frame.DataFrame):
raise RuntimeError(f'df_table is not a DataFrame ({type(df_table)}):\n{df_table}')
df_table = df_table.reset_index()
with tags.div(_class='table-responsive').add(tags.table(_class=table_class)):
# Create header row
with tags.thead().add(tags.tr()):
for col in df_table.columns:
tags.th(col)
# Create body rows
with tags.tbody():
for row in df_table.itertuples(index=False):
with tags.tr():
for value in row:
tags.td(str(value))
def write_lookup(key, function_lookup):
"""Determine the lookup result and add to the file.
Args:
key: string key for function lookup
function_lookup: dictionary with either the string result or equation and arguments
Raises:
RuntimeError: if error in lookup dictionary
"""
try:
match = function_lookup[key]
except KeyError:
raise RuntimeError(f'Could not find "{key}" in {function_lookup}')
if isinstance(match, str):
util.raw(match)
elif len(match) == 2:
fun, args = match
result = fun(*args)
if isinstance(result, str):
util.raw(result)
else:
raise RuntimeError(f'Match failed for "{key}". Returned: {match} from {function_lookup}')
def markdown_machine(lines, function_lookup): # noqa: CCR001
"""Convert markdown text file into Plotly-HTML and write to doc context.
Note: you will need a document with necessary boilerplate and call this within a `with doc:` dominate context
Multiple Markdown files can then be put into a single HTML output file by calling this function with new lines
and function lookup arguments
Args:
lines: list of text file lines
function_lookup: dictionary with either the string result or equation and arguments
Will be inserted into file where `>>lookup:function_name` assuming key of `function_name`
"""
markdown_lines = []
for line in lines:
if line.startswith('>>lookup:'):
# Write stored markdown and clear list. Then write the matched lookup
tag_markdown('\n'.join(markdown_lines))
markdown_lines = []
write_lookup(line.split('>>lookup:')[1], function_lookup)
else:
markdown_lines.append(line)
if markdown_lines:
tag_markdown('\n'.join(markdown_lines))
def write_from_markdown(filename, function_lookup, **dbc_kwargs):
"""Wrap markdown_machine to convert markdown to Bootstrap HTML.
Args:
filename: path to markdown file
function_lookup: dictionary with either the string result or equation and arguments
Will be inserted into file where `>>lookup:function_name` assuming key of `function_name`
dbc_kwargs: keyword arguments to pass to `create_dbc_doc`
Returns:
Path: created HTML filename
"""
lines = filename.read_text().split('\n')
html_filename = filename.parent / f'{filename.stem}.html'
doc = create_dbc_doc(**dbc_kwargs)
with doc:
with tags.div(_class='container').add(tags.div(_class='col')):
markdown_machine(lines, function_lookup)
html_filename.write_text(str(doc))
return html_filename
Functions⚓︎
add_image⚓︎
def add_image(
image_path,
alt_text=None
)
Write base64 image to HTML.
Parameters:
| Name | Description |
|---|---|
| image_path | Path to image file and format will be read from file suffix. |
| alt_text | alternate text. If None, will show the image filename |
Returns:
| Type | Description |
|---|---|
| str | HTML |
View Source
def add_image(image_path, alt_text=None):
"""Write base64 image to HTML.
Args:
image_path: Path to image file and format will be read from file suffix.
alt_text: alternate text. If None, will show the image filename
Returns:
str: HTML
"""
with open(image_path, 'rb') as image_file:
encoded_image = b64encode(image_file.read()).decode()
image_uri = f'data:image/{image_path.suffix[1:]};base64,{encoded_image}'
return f'<img src="{image_uri}" alt="{alt_text or image_path.name}"/>'
add_video⚓︎
def add_video(
video_path,
alt_text=None
)
Write base64 video to HTML.
Video formats can easily be converted with ffmpeg: ffmpeg -i video_filename.mov video_filename.webm
Parameters:
| Name | Description |
|---|---|
| video_path | Path to video file and format will be read from file suffix. Video should be in webm format |
| alt_text | alternate text. If None, will show the video filename |
Returns:
| Type | Description |
|---|---|
| str | HTML video tag |
View Source
def add_video(video_path, alt_text=None):
"""Write base64 video to HTML.
Video formats can easily be converted with ffmpeg: `ffmpeg -i video_filename.mov video_filename.webm`
Args:
video_path: Path to video file and format will be read from file suffix. Video should be in webm format
alt_text: alternate text. If None, will show the video filename
Returns:
str: HTML video tag
"""
with open(video_path, 'rb') as video_file:
encoded_video = b64encode(video_file.read()).decode()
video_uri = f'data:video/{video_path.suffix[1:]};base64,{encoded_video}'
return f'<video src="{video_uri}" controls>{alt_text or video_path.name}</video>'
capture_plotly_body⚓︎
def capture_plotly_body()
Return HTML body that includes necessary scripts for Plotly and MathJax.
Returns:
| Type | Description |
|---|---|
| tuple | of the top and the bottom HTML content |
View Source
def capture_plotly_body():
"""Return HTML body that includes necessary scripts for Plotly and MathJax.
Returns:
tuple: of the top and the bottom HTML content
"""
# Capture necessary Plotly boilerplate HTML
with io.StringIO() as output:
write_div({}, output, is_div=False, include_mathjax='.js', validate=False)
blank_plotly = BeautifulSoup(output.getvalue(), features='lxml')
# Remove the empty figure div and corresponding script
plot_div = blank_plotly.find('div', attrs={'class': 'plotly-graph-div'})
for script in blank_plotly.find_all('script')[::-1]:
# Use the ID from the plot to identify which script needs to be removed
if plot_div.attrs['id'] in script.prettify():
script.decompose()
break
plot_div.decompose()
return blank_plotly.body.prettify()
create_dbc_doc⚓︎
def create_dbc_doc(
theme='https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css',
custom_styles='',
**doc_kwargs
)
Create boilerplate dominate document with Bootstrap and Plotly for static HTML.
Based on: https://github.com/facultyai/dash-bootstrap-components/tree/master/docs/templates/partials
See dominate documentation: https://pypi.org/project/dominate/
Parameters:
| Name | Description |
|---|---|
| theme | string URL to CSS for theming Bootstrap. Default is dbc.themes.BOOTSTRAP |
| custom_styles | optional custom CSS to add to file. Default is blank ('') |
| doc_kwargs | keyword arguments for dominate.document() |
Returns:
| Type | Description |
|---|---|
| dict | dominate document instance |
View Source
def create_dbc_doc(theme=dbc.themes.BOOTSTRAP, custom_styles='', **doc_kwargs):
"""Create boilerplate dominate document with Bootstrap and Plotly for static HTML.
Based on: https://github.com/facultyai/dash-bootstrap-components/tree/master/docs/templates/partials
See dominate documentation: https://pypi.org/project/dominate/
Args:
theme: string URL to CSS for theming Bootstrap. Default is `dbc.themes.BOOTSTRAP`
custom_styles: optional custom CSS to add to file. Default is blank (`''`)
doc_kwargs: keyword arguments for `dominate.document()`
Returns:
dict: dominate document instance
"""
stylesheets = [
{'href': 'https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/a11y-light.min.css'},
{'href': theme},
]
scripts = [
{'src': 'https://code.jquery.com/jquery-3.4.1.slim.min.js'},
{'src': 'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js'},
{'src': 'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js'},
{'src': 'https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js'},
]
doc = format_plotly_boilerplate(**doc_kwargs)
with doc.head:
tags.meta(charset='utf-8')
tags.meta(name='viewport', content='width=device-width, initial-scale=1')
for sheet_kwargs in stylesheets:
tags.link(rel='stylesheet', **sheet_kwargs)
util.raw(f'<style>{custom_styles}</style>')
for script_kwargs in scripts:
tags.script(**script_kwargs)
util.raw('<script>hljs.initHighlightingOnLoad();</script>')
return doc
format_plotly_boilerplate⚓︎
def format_plotly_boilerplate(
**doc_kwargs
)
Initialize a boilerplate dominate document for creating static Plotly HTML files.
See dominate documentation: https://pypi.org/project/dominate/
Parameters:
| Name | Description |
|---|---|
| doc_kwargs | keyword arguments for dominate.document() |
Returns:
| Type | Description |
|---|---|
| dict | dominate document instance |
View Source
def format_plotly_boilerplate(**doc_kwargs):
"""Initialize a boilerplate dominate document for creating static Plotly HTML files.
See dominate documentation: https://pypi.org/project/dominate/
Args:
doc_kwargs: keyword arguments for `dominate.document()`
Returns:
dict: dominate document instance
"""
doc = dominate.document(**doc_kwargs)
with doc:
util.raw(capture_plotly_body())
return doc
make_div⚓︎
def make_div(
figure,
**html_kwargs
)
Write Plotly figure as HTML to specified file.
Parameters:
| Name | Description |
|---|---|
| figure | Plotly figure (can be from create_figure for custom charts) |
| html_kwargs | additional keyword arguments passed to plotly.io.write_html() |
Returns:
| Type | Description |
|---|---|
| str | HTML div |
View Source
def make_div(figure, **html_kwargs):
"""Write Plotly figure as HTML to specified file.
Args:
figure: Plotly figure (can be from `create_figure` for custom charts)
html_kwargs: additional keyword arguments passed to `plotly.io.write_html()`
Returns:
str: HTML div
"""
with io.StringIO() as output:
write_div(figure, output, is_div=True, **html_kwargs)
return output.getvalue()
markdown_machine⚓︎
def markdown_machine(
lines,
function_lookup
)
Convert markdown text file into Plotly-HTML and write to doc context.
Note: you will need a document with necessary boilerplate and call this within a with doc: dominate context
Multiple Markdown files can then be put into a single HTML output file by calling this function with new lines
and function lookup arguments
Parameters:
| Name | Description |
|---|---|
| lines | list of text file lines |
| function_lookup | dictionary with either the string result or equation and arguments Will be inserted into file where >>lookup:function_name assuming key of function_name |
View Source
def markdown_machine(lines, function_lookup): # noqa: CCR001
"""Convert markdown text file into Plotly-HTML and write to doc context.
Note: you will need a document with necessary boilerplate and call this within a `with doc:` dominate context
Multiple Markdown files can then be put into a single HTML output file by calling this function with new lines
and function lookup arguments
Args:
lines: list of text file lines
function_lookup: dictionary with either the string result or equation and arguments
Will be inserted into file where `>>lookup:function_name` assuming key of `function_name`
"""
markdown_lines = []
for line in lines:
if line.startswith('>>lookup:'):
# Write stored markdown and clear list. Then write the matched lookup
tag_markdown('\n'.join(markdown_lines))
markdown_lines = []
write_lookup(line.split('>>lookup:')[1], function_lookup)
else:
markdown_lines.append(line)
if markdown_lines:
tag_markdown('\n'.join(markdown_lines))
tag_code⚓︎
def tag_code(
text,
language=''
)
Format HTML for a pre.code block with specified hljs language.
Parameters:
| Name | Description |
|---|---|
| text | string to show in code block |
| language | hljs language (ex: 'language-json'). Default is ''. |
View Source
def tag_code(text, language=''):
"""Format HTML for a `pre.code` block with specified `hljs` language.
Args:
text: string to show in code block
language: `hljs` language (ex: `'language-json'`). Default is `''`.
"""
tags.pre().add(tags.code(text, _class=f'{language} hljs'))
tag_markdown⚓︎
def tag_markdown(
text,
**markdown_kwargs
)
Format markdown text as HTML.
Parameters:
| Name | Description |
|---|---|
| text | markdown string |
| markdown_kwargs | additional keyword arguments for markdown.markdown, such as extensions |
View Source
def tag_markdown(text, **markdown_kwargs):
"""Format markdown text as HTML.
Args:
text: markdown string
markdown_kwargs: additional keyword arguments for `markdown.markdown`, such as `extensions`
"""
util.raw(markdown.markdown(text, **markdown_kwargs))
tag_table⚓︎
def tag_table(
df_table,
table_class=None
)
Format HTML for a responsive Bootstrap table.
See Bootstrap documentation at: https://getbootstrap.com/docs/4.4/content/tables/#tables
Parameters:
| Name | Description |
|---|---|
| df_table | pandas dataframe to show in table |
| table_class | string classes to add to table. If None, will use default string |
Raises:
| Type | Description |
|---|---|
| RuntimeError | if df_table is not a DataFrame object |
View Source
def tag_table(df_table, table_class=None):
"""Format HTML for a responsive Bootstrap table.
See Bootstrap documentation at: https://getbootstrap.com/docs/4.4/content/tables/#tables
Args:
df_table: pandas dataframe to show in table
table_class: string classes to add to table. If None, will use default string
Raises:
RuntimeError: if `df_table` is not a DataFrame object
"""
if table_class is None:
table_class = 'table table-bordered table-striped table-hover'
if not isinstance(df_table, pd.core.frame.DataFrame):
raise RuntimeError(f'df_table is not a DataFrame ({type(df_table)}):\n{df_table}')
df_table = df_table.reset_index()
with tags.div(_class='table-responsive').add(tags.table(_class=table_class)):
# Create header row
with tags.thead().add(tags.tr()):
for col in df_table.columns:
tags.th(col)
# Create body rows
with tags.tbody():
for row in df_table.itertuples(index=False):
with tags.tr():
for value in row:
tags.td(str(value))
write_div⚓︎
def write_div(
figure,
path_or_file_object,
is_div=True,
**html_kwargs
)
Write Plotly figure as HTML to specified file.
Parameters:
| Name | Description |
|---|---|
| figure | Plotly figure (can be from create_figure for custom charts) |
| path_or_file_object | string path or file object |
| is_div | if True (default) will override html_kwargs to only write the minimum HTML needed |
| html_kwargs | additional keyword arguments passed to plotly.io.write_html() |
View Source
def write_div(figure, path_or_file_object, is_div=True, **html_kwargs):
"""Write Plotly figure as HTML to specified file.
Args:
figure: Plotly figure (can be from `create_figure` for custom charts)
path_or_file_object: *string* path or file object
is_div: if True (default) will override html_kwargs to only write the minimum HTML needed
html_kwargs: additional keyword arguments passed to `plotly.io.write_html()`
"""
for key in ['include_plotlyjs', 'full_html']:
if key not in html_kwargs and is_div:
html_kwargs[key] = False
plotly.io.write_html(fig=figure, file=path_or_file_object, **html_kwargs)
write_from_markdown⚓︎
def write_from_markdown(
filename,
function_lookup,
**dbc_kwargs
)
Wrap markdown_machine to convert markdown to Bootstrap HTML.
Parameters:
| Name | Description |
|---|---|
| filename | path to markdown file |
| function_lookup | dictionary with either the string result or equation and arguments Will be inserted into file where >>lookup:function_name assuming key of function_name |
| dbc_kwargs | keyword arguments to pass to create_dbc_doc |
Returns:
| Type | Description |
|---|---|
| Path | created HTML filename |
View Source
def write_from_markdown(filename, function_lookup, **dbc_kwargs):
"""Wrap markdown_machine to convert markdown to Bootstrap HTML.
Args:
filename: path to markdown file
function_lookup: dictionary with either the string result or equation and arguments
Will be inserted into file where `>>lookup:function_name` assuming key of `function_name`
dbc_kwargs: keyword arguments to pass to `create_dbc_doc`
Returns:
Path: created HTML filename
"""
lines = filename.read_text().split('\n')
html_filename = filename.parent / f'{filename.stem}.html'
doc = create_dbc_doc(**dbc_kwargs)
with doc:
with tags.div(_class='container').add(tags.div(_class='col')):
markdown_machine(lines, function_lookup)
html_filename.write_text(str(doc))
return html_filename
write_image_file⚓︎
def write_image_file(
figure,
path_or_file_object,
image_format,
**img_kwargs
)
Write Plotly figure as an image to specified file.
Parameters:
| Name | Description |
|---|---|
| figure | Plotly figure (can be from create_figure for custom charts) |
| path_or_file_object | string path or file object |
| image_format | one of (png, jpg, jpeg, webp, svg, pdf) |
| img_kwargs | additional keyword arguments passed to plotly.io.write_image() |
View Source
def write_image_file(figure, path_or_file_object, image_format, **img_kwargs):
"""Write Plotly figure as an image to specified file.
Args:
figure: Plotly figure (can be from `create_figure` for custom charts)
path_or_file_object: *string* path or file object
image_format: one of `(png, jpg, jpeg, webp, svg, pdf)`
img_kwargs: additional keyword arguments passed to `plotly.io.write_image()`
"""
plotly.io.write_image(fig=figure, file=str(path_or_file_object), format=image_format, **img_kwargs)
write_lookup⚓︎
def write_lookup(
key,
function_lookup
)
Determine the lookup result and add to the file.
Parameters:
| Name | Description |
|---|---|
| key | string key for function lookup |
| function_lookup | dictionary with either the string result or equation and arguments |
Raises:
| Type | Description |
|---|---|
| RuntimeError | if error in lookup dictionary |
View Source
def write_lookup(key, function_lookup):
"""Determine the lookup result and add to the file.
Args:
key: string key for function lookup
function_lookup: dictionary with either the string result or equation and arguments
Raises:
RuntimeError: if error in lookup dictionary
"""
try:
match = function_lookup[key]
except KeyError:
raise RuntimeError(f'Could not find "{key}" in {function_lookup}')
if isinstance(match, str):
util.raw(match)
elif len(match) == 2:
fun, args = match
result = fun(*args)
if isinstance(result, str):
util.raw(result)
else:
raise RuntimeError(f'Match failed for "{key}". Returned: {match} from {function_lookup}')
Created: August 5, 2022